#include "Highlighter.h"
#include "CodeEdit.h"

#include "simodo/module/ModuleCollector_interface.h"
#include "simodo/variable/Variable.h"

#include <QTextDocument>
#include <QPalette>

using namespace simodo;

Highlighter::Highlighter(QTextDocument * document, const Highlighter* highlighter, SemanticFormatOrder semantic_format_order, ContrastMode contrast_mode)
    : QSyntaxHighlighter(document)
    , _highlight_data(highlighter->highlight_data())
    , _diagnostics(highlighter->diagnostics())
    , _semantic_tokens(highlighter->semantic_tokens())
    , _semantic_format_order(semantic_format_order)
    , _contrast_mode(contrast_mode)
    , _current_formats(highlighter->current_formats())
{
}

Highlighter::Highlighter(QTextDocument * document, HighlightData highlight_data, const QPalette & palette,
                         const QVector<simodo::shell::Diagnostic> & diagnostics,
                         const std::multimap<int,simodo::shell::SemanticToken> & semantic_tokens, SemanticFormatOrder semantic_format_order)
    : QSyntaxHighlighter(document)
    , _highlight_data(highlight_data)
    , _diagnostics(diagnostics)
    , _semantic_tokens(semantic_tokens)
    , _semantic_format_order(semantic_format_order)
{
    setupCurrentFormat(palette);
}

void Highlighter::setupCurrentFormat(const QPalette & palette)
{
    if (palette.window().color().toCmyk().black() > 128) {
        // qDebug() << "Change to dark theme";
        setupCurrentFormat(highlight_data().dark);
    }
    else {
        // qDebug() << "Change to light theme";
        setupCurrentFormat(highlight_data().light);
    }
}

const QTextCharFormat & Highlighter::findFormat(const std::u16string & type) const
{
    auto it = _current_formats.find(type);

    if (it != _current_formats.end())
        return it->second;
        
    static const QTextCharFormat fmt;
    return fmt;
}

void Highlighter::highlightBlock(const QString &text)
{
    /// \todo На больших файлах подсветка тормозит. Желательно посмотреть, что можно оптимизировать.
    /// 1. Как вариант, попробовать выполнять раскраску по частям, т.е. только для видимого участка текста
    /// 2. Может тормозить поиск по _semantic_tokens. Эта структура изначально упорядочена, её можно
    /// преобразовать в вектор, запоминать последнюю строку и если рисуем следующую, то ок, а иначе...

    int line = currentBlock().blockNumber();

    if (_semantic_format_order == SemanticFormatOrder::Early)
        formatSemanticTokens(line);

    for(auto & object : highlight_data().tokenizers) {
        if (!object)
            continue;
        highlightElement(object, text.toStdU16String(), currentBlock().position()/*, data*/);
    }

    if (_semantic_format_order == SemanticFormatOrder::Post)
        formatSemanticTokens(line);

    formatDiagnostic(line);
}

void Highlighter::highlightElement(std::shared_ptr<variable::Object> object, const std::u16string &text, int pos)
{
    variable::Value produceTokens_result = object->invoke(u"produceTokens", {
                                                {u"text_to_parse", text},
                                                {u"position", pos},
                                                {u"context", static_cast<int64_t>(previousBlockState())}
                                            });
    if (produceTokens_result.type() != variable::ValueType::Object || produceTokens_result.getObject()->variables().empty())
        return;

    std::shared_ptr<variable::Object> tokens_data   = produceTokens_result.getObject();
    variable::Value &                 tokens_value  = tokens_data->find(u"tokens");
    if (tokens_value.type() != variable::ValueType::Array)
        return;

    variable::Value & context_value = tokens_data->find(u"context");
    if (context_value.type() == variable::ValueType::Int)
        setCurrentBlockState(context_value.getInt());

    for(const variable::Value & token_value : tokens_value.getArray()->values()) {
        if (token_value.type() != variable::ValueType::Object)
            continue;

        const std::shared_ptr<variable::Object> token       = token_value.getObject();
        const variable::Value &                 t_token     = token->find(u"token");
        const variable::Value &                 t_position  = token->find(u"position");
        const variable::Value &                 t_type      = token->find(u"type");

        if (t_position.type() == variable::ValueType::Int &&
            t_token.type() == variable::ValueType::String &&
            t_type.type() == variable::ValueType::String ) {

            int             character = t_position.getInt()-currentBlock().position();
            int             length    = t_token.getString().length();
            std::u16string  type      = t_type.getString();
            
            if (type != u"Word")
                // см. simodo/inout/token/LexemeType.h, inout::getLexemeTypeName
                setFormat(character, length, findFormat(type));

            for(auto & obj_pair : highlight_data().tokenizers_spec) {
                if (!obj_pair.first)
                    continue;

                if (obj_pair.second == t_type.getString())
                    highlightElement(obj_pair.first, t_token.getString(), t_position.getInt()/*, data*/);
            }
        }
    }
}

void Highlighter::setupCurrentFormat(std::shared_ptr<simodo::variable::Object> theme)
{
    _current_formats.clear();
    for(const variable::Variable & variable : theme->variables()) 
        _current_formats.insert({variable.name(), buildFormat(variable.value())});
}

QTextCharFormat Highlighter::buildFormat(const simodo::variable::Value & fmt)
{
    QTextCharFormat format;

    if (fmt.type() != variable::ValueType::Object)
        return format;

    const std::shared_ptr<simodo::variable::Object> fmt_elemets = fmt.getObject();

    const variable::Value & fmt_background = fmt_elemets->find(u"background");
    if (fmt_background.type() == variable::ValueType::String)
        format.setBackground(QColor(QString::fromStdU16String(fmt_background.getString())));

    const variable::Value & fmt_foreground = fmt_elemets->find(u"foreground");
    if (fmt_foreground.type() == variable::ValueType::String)
        format.setForeground(QColor(QString::fromStdU16String(fmt_foreground.getString())));

    const variable::Value & fmt_strikeout = fmt_elemets->find(u"strikeout");
    if (fmt_strikeout.type() == variable::ValueType::Bool)
        format.setFontStrikeOut(fmt_strikeout.getBool());

    const variable::Value & fmt_overline = fmt_elemets->find(u"overline");
    if (fmt_overline.type() == variable::ValueType::Bool)
        format.setFontOverline(fmt_overline.getBool());

    const variable::Value & fmt_underline = fmt_elemets->find(u"underline");
    if (fmt_underline.type() == variable::ValueType::Bool)
        format.setFontUnderline(fmt_underline.getBool());

    const variable::Value & fmt_italic = fmt_elemets->find(u"italic");
    if (fmt_italic.type() == variable::ValueType::Bool) 
        format.setFontItalic(fmt_italic.getBool());

    const variable::Value & fmt_underline_color = fmt_elemets->find(u"underline_color");
    if (fmt_underline_color.type() == variable::ValueType::String)
        format.setUnderlineColor(QColor(QString::fromStdU16String(fmt_underline_color.getString())));

    const variable::Value & fmt_underline_style = fmt_elemets->find(u"underline_style");
    if (fmt_underline_style.type() == variable::ValueType::String) {

        const std::u16string &          underline_style_name = fmt_underline_style.getString();
        QTextCharFormat::UnderlineStyle us                   = QTextCharFormat::NoUnderline;

        if (underline_style_name == u"SingleUnderline")
            us = QTextCharFormat::SingleUnderline;
        else if (underline_style_name == u"DashUnderline")
            us = QTextCharFormat::DashUnderline;
        else if (underline_style_name == u"DotLine")
            us = QTextCharFormat::DotLine;
        else if (underline_style_name == u"DashDotLine")
            us = QTextCharFormat::DashDotLine;
        else if (underline_style_name == u"DashDotDotLine")
            us = QTextCharFormat::DashDotDotLine;
        else if (underline_style_name == u"WaveUnderline")
            us = QTextCharFormat::WaveUnderline;
        else if (underline_style_name == u"SpellCheckUnderline")
            us = QTextCharFormat::SpellCheckUnderline;

        format.setUnderlineStyle(us);
    }

    const variable::Value & fmt_weight = fmt_elemets->find(u"weight");
    if (fmt_weight.type() == variable::ValueType::String) {

        const std::u16string & weight_name = fmt_weight.getString();
        int                    weight      = QFont::Normal;

        if (weight_name == u"Thin")
            weight = QFont::Thin;
        else if (weight_name == u"ExtraLight")
            weight = QFont::ExtraLight;
        else if (weight_name == u"Light")
            weight = QFont::Light;
        else if (weight_name == u"Normal")
            weight = QFont::Normal; // -V1048
        else if (weight_name == u"Medium")
            weight = QFont::Medium;
        else if (weight_name == u"DemiBold")
            weight = QFont::DemiBold;
        else if (weight_name == u"Bold")
            weight = QFont::Bold;
        else if (weight_name == u"ExtraBold")
            weight = QFont::ExtraBold;
        else if (weight_name == u"Black")
            weight = QFont::Black;

        format.setFontWeight(weight);
    }

    return format;
}

void Highlighter::formatSemanticTokens(int line)
{
    auto range = _semantic_tokens.equal_range(line);
    for(auto it = range.first; it != range.second; ++it) {
        QTextCharFormat format = findFormat(it->second.tokenType);
        for(auto & m : it->second.tokenModifiers)
            format.merge(findFormat(u"m_"+m));

        /// @note Хайлайтер работает с форматированием документа, что приводит к тому, 
        /// что подчёркивается слово во всех открытых редакторах этого документа. 
        if (_anchor.line == currentBlock().blockNumber()
         && _anchor.character == it->second.startChar && _anchor.length == it->second.length)
            format.setFontUnderline(true);

        setFormat(it->second.startChar, it->second.length, format);
    }
}

void Highlighter::formatDiagnostic(int line)
{
    /// @todo Не оптимально? Нужно проверять по заранее подготовленному индексу строк сообщений?
    /// (Вообще-то, если работает ограничение на кол-во сообщений об ошибках (например в 50 штук), то оптимизация не нужна)
    /// При оптимизации нужно учесть, что диагностическое сообщение может распространяться на несколько строк
    for(const shell::Diagnostic & d : _diagnostics)
        if (d.range.start().line() <= simodo::inout::position_line_t(line) && d.range.end().line() >= simodo::inout::position_line_t(line)) {
            int start = d.range.start().line() == simodo::inout::position_line_t(line)
                        ? d.range.start().character()
                        : 0;
            int count = d.range.end().line() == simodo::inout::position_line_t(line)
                        ? d.range.end().character() - start
                        : currentBlock().length() - start;
            int length = currentBlock().length();

            QTextCharFormat diagnostic_fmt;

            if (_contrast_mode == ContrastMode::Hight)
                diagnostic_fmt = findFormat(u"hc_diagnostic-"+lsp::getDiagnosticSeverityString(d.severity));
            else
                diagnostic_fmt = findFormat(u"diagnostic-"+lsp::getDiagnosticSeverityString(d.severity));

            for(int i=0; i < count; ++i) {
                if (i >= length)
                    break;

                QTextCharFormat fmt = format(start+i);

                fmt.merge(diagnostic_fmt);
                setFormat(start+i, 1, fmt);
            }
        }
}

